//******************************************************************
//******************************************************************
//**********          ANts Peer To Peer Sources        *************
//
// ANts P2P realizes a third generation P2P net. It protects your
// privacy while you are connected and makes you not trackable, hiding
// your identity (ip) and crypting everything you are sending/receiving
// from others.

// Copyright (C) 2004  Roberto Rossi

// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

package ants.p2p.security.sockets;

import java.net.*;
import java.io.*;
import java.security.*;
import java.security.spec.*;
import java.util.LinkedList;
import java.util.concurrent.Semaphore;

import javax.net.*;
import javax.net.ssl.*;

import kerjodando.vincent.Common;

import ants.p2p.*;
import ants.p2p.utils.addresses.InetAddressWatchdog;
import ants.p2p.utils.addresses.AddressServerThread;

import net.nutss.stunt.STUNTEndpoint;
import net.nutss.stunt.STUNTEventAdapter;

import org.apache.log4j.*;

public class SecureServer implements Runnable {
	String KEYSTORE = "certs";
	char[] KEYSTOREPW = "serverkspw".toCharArray();
	char[] KEYPW = "serverpw".toCharArray();
	int serverPort = 443;
	ServerSocket serverSocket = null;

	static Logger _logger = Logger.getLogger(SecureServer.class.getName());

	public Thread serverThread;
	KerjoSTUNTServer sserver;

	Ant n;

	public SecureServer(Ant n, int port) throws Exception {
		this.n = n;
		this.serverPort = port;
		this.serverSocket = this.createServerSocket();
		try {
			serverThread = new Thread(this);
			serverThread.setPriority(10);
			serverThread.start();
		} catch (Exception e) {
			_logger.error("", e);
		}
	}

	public SecureServer(Ant n) throws Exception, NoSuchAlgorithmException,
			InvalidParameterSpecException, InvalidAlgorithmParameterException,
			InvalidKeyException {
		this.n = n;
		this.serverSocket = this.createServerSocket();
		serverThread = new Thread(this);
		serverThread.setPriority(10);
		serverThread.start();
	}

	public void listen() {
		while (!n.isDisconnected() && !this.serverSocket.isClosed()) {
			try {
				_logger.info(n.getShortId() + " :Server Listening...");
				Socket localSocket = this.serverSocket.accept();
				if (localSocket != null) {
					RequestsHandler handler = new RequestsHandler(n,
							localSocket);
					handler.start();
				}
			} catch (Exception e) {
				_logger.error("Secure Server Socket listen cycle: "
						+ this.serverSocket.getLocalPort() + " ", e);
				this.n.disconnect();
			}
		}
		_logger.info("Secure Server Socket closed: "
				+ this.serverSocket.getLocalPort());
	}

	public void run() {
		listen();
	}

	public ServerSocket getServerSocket() {
		return this.serverSocket;
	}

	public void runSTUN() {
		try {
			// if a STUNT Server is needed or the old server dies
			if (Common.NeedSTUNT && (sserver == null || !sserver.isAlive())) {
				_logger.info("start stunt server!");
				sserver = new KerjoSTUNTServer(this.n, new URI(
						Common.identifier_id + "@84.234.17.41"));
				sserver.start();
			}
		} catch (Exception e) {
		}
	}

	public void stopSTUN() {
		try {
			if (sserver != null) {
				sserver.setTerm(true);
				sserver.interrupt();
				sserver = null;
			}
		} catch (Exception e) {

		}
	}

	private ServerSocket createServerSocket() throws Exception {
		// Make sure that JSSE is available
		Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());
		// A keystore is where keys and certificates are kept
		// Both the keystore and individual private keys should be password
		// protected
		KeyStore keystore = KeyStore.getInstance("JKS");
		keystore.load(this.getClass().getClassLoader().getResource(KEYSTORE)
				.openStream(), KEYSTOREPW);
		// A KeyManagerFactory is used to create key managers
		KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
		// Initialize the KeyManagerFactory to work with our keystore
		kmf.init(keystore, KEYPW);
		// An SSLContext is an environment for implementing JSSE
		// It is used to create a ServerSocketFactory
		SSLContext sslc = SSLContext.getInstance("TLSv1");
		// Initialize the SSLContext to work with our key managers
		sslc.init(kmf.getKeyManagers(), null, null);
		// Create a ServerSocketFactory from the SSLContext
		ServerSocketFactory ssf = sslc.getServerSocketFactory();
		// Socket to me
		SSLServerSocket serverSocket = (SSLServerSocket) ssf
				.createServerSocket(serverPort);
		// Return a ServerSocket on the desired port (443)
		return serverSocket;
	}
}

class KerjoSTUNTServer extends Thread {
	static Logger _logger = Logger.getLogger(KerjoSTUNTServer.class.getName());
	public Semaphore semaphore = new Semaphore(0);
	STUNTEndpoint sock;

	LinkedList sockets = new LinkedList();
	// LinkedList uris = new LinkedList();
	URI i;
	Ant n;

	public KerjoSTUNTServer(Ant n1, URI uri) {
		i = uri;
		n = n1;
	}

	public void setTerm(boolean tr) {
		if (sock != null) {
			sock.setTerm(tr);
			sock.close();
		}

	}

	public boolean getTerm() {
		return (sock != null) ? sock.getTerm() : true;

	}

	class Handler extends STUNTEventAdapter {
		KerjoSTUNTServer sever1;

		public Handler(KerjoSTUNTServer sever) {
			sever1 = sever;
		}

		public void acceptTCPHandler(Socket socket, URI remoteAddress) {
			synchronized (sever1.semaphore) {
				sever1.sockets.addLast(socket);
				// uris.addLast(remoteAddress);
				sever1.semaphore.release();
			}
		}

		public void errorHandler(Exception e) {
			synchronized (sever1.semaphore) {
				while (!sever1.sockets.isEmpty()) {
					Socket s = (Socket) sever1.sockets.removeFirst();

					// String suri=
					// sever1.uris.removeFirst().toString().split("@")[0];
					try {
						s.close();
					} catch (Exception e1) {
					}
				}
				sever1.semaphore.release();
			}
			_logger.error("STUNT Server Socket listen cycle:  ", e);
		}
	}

	public void run() {

		try {
			_logger.info("STUNTSERVEr running");
			sock = new STUNTEndpoint(i);
			sock.setTerm(false);
			sock.listen(new Handler(this));
			while (true) {
				semaphore.acquire();
				Socket s;
				synchronized (semaphore) {
					s = sockets.isEmpty() ? null : (Socket) sockets
							.removeFirst();
				}
				if (s == null) {
					setTerm(true);
					break;
				}
				try {
					String KEYSTORE = "certs";
					char[] KEYSTOREPW = "serverkspw".toCharArray();
					char[] KEYPW = "serverpw".toCharArray();
					// Make sure that JSSE is available
					Security
							.addProvider(new com.sun.net.ssl.internal.ssl.Provider());
					// A keystore is where keys and certificates are kept
					// Both the keystore and individual private keys should be
					// password protected
					KeyStore keystore = KeyStore.getInstance("JKS");
					keystore.load(this.getClass().getClassLoader().getResource(
							KEYSTORE).openStream(), KEYSTOREPW);
					// A KeyManagerFactory is used to create key managers
					KeyManagerFactory kmf = KeyManagerFactory
							.getInstance("SunX509");
					// Initialize the KeyManagerFactory to work with our
					// keystore
					kmf.init(keystore, KEYPW);
					TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
						public java.security.cert.X509Certificate[] getAcceptedIssuers() {
							return new java.security.cert.X509Certificate[0];
						}

						public void checkClientTrusted(
								java.security.cert.X509Certificate[] certs,
								String authType) {
						}

						public void checkServerTrusted(
								java.security.cert.X509Certificate[] certs,
								String authType) {
						}
					} };
					// An SSLContext is an environment for implementing JSSE
					// It is used to create a ServerSocketFactory
					SSLContext sslc = SSLContext.getInstance("TLSv1");
					// Initialize the SSLContext to work with our key managers
					sslc.init(kmf.getKeyManagers(), trustAllCerts,
							new java.security.SecureRandom());
					SSLSocketFactory ssf = sslc.getSocketFactory();

					_logger.info("STUNT Server get an input:  "
							+ s.getInetAddress().getHostAddress() + ":"
							+ s.getPort());
					SSLSocket s2 = (SSLSocket) ssf.createSocket(s, s
							.getInetAddress().getHostAddress(), s.getPort(),
							true);
					if (s2 != null) {
						s2.setUseClientMode(false);
						// s2.setNeedClientAuth(true);
						s2.startHandshake();
						RequestsHandler handler = new RequestsHandler(n, s2);
						handler.start();
					}
				} catch (Exception e3) {
					try {
						s.close();
					} catch (Exception e1) {
					}

				}

			}
		} catch (InterruptedException e1) {
		} catch (Exception e) {
			_logger.error("STUNT Server Socket listen error:  ", e);
		}
	}
}

class RequestsHandler extends Thread {
	Socket localSocket;
	Ant n;

	static Logger _logger = Logger.getLogger(RequestsHandler.class.getName());

	public RequestsHandler(Ant caller, Socket localSocket) {
		this.localSocket = localSocket;
		this.n = caller;
	}

	public void run() {
		try {
			int type = this.localSocket.getInputStream().read();
			if (type == 0) {
				_logger.info("Connection type: 0");
				NewClientHandler handler = new NewClientHandler(this.n,
						this.localSocket);
				handler.start();
			} else if (type == 1) {
				_logger.info("Connection type: 1");
				AddressesRequestHandler handler = new AddressesRequestHandler(
						this.n, this.localSocket);
				handler.start();
			} else if (type == 2 && n.acceptTCPDirectConnections()) {
				_logger.info("Connection type: 2");
				DirectNeighbourRequestHandler handler = new DirectNeighbourRequestHandler(
						this.n, this.localSocket);
				handler.start();
			} else {
				localSocket.close();
			}
		} catch (IOException ex) {
			_logger.error("", ex);
			return;
		}
	}
}

class NewClientHandler extends Thread {
	Socket localSocket;
	Ant n;

	static Logger _logger = Logger.getLogger(NewClientHandler.class.getName());

	public NewClientHandler(Ant caller, Socket localSocket) {
		this.localSocket = localSocket;
		this.n = caller;
		this.setPriority(5);
	}

	public void run() {
		try {
			String remoteAddress = localSocket.getInetAddress()
					.getHostAddress();
			if (!InetAddressWatchdog.getInstance().allowedAddress(
					InetAddress.getByName(remoteAddress).getHostAddress())) {
				localSocket.close();
				throw new Exception("Address is not allowed: " + remoteAddress);
			}
			if (n.getNeighboursNumber() < n.getMaxNeighbours()) {
				localSocket.setKeepAlive(true);
				SecureServerSocketThread serverSocketThread = new SecureServerSocketThread(
						localSocket);
				/* not need again by casper*/
				/*
				 * if (serverSocketThread.isNewVersionDetected()) {
				 * this.n.getPropertyChangeSupport().firePropertyChange(
				 * "newANtsVersionDetected", null,
				 * serverSocketThread.getNewerVersion()); }
				 */
				if (!localSocket.isClosed()) {
					_logger.info(" Local time elapsed: "
							+ serverSocketThread.getTimeElapsed()
							+ "[Thresold: " + n.getRateThresold() + "]");
					/*
					 * _logger.info(this.caller.serverSocket.getInetAddress().getHostAddress() + ":
					 * Local time elapsed: " +
					 * serverSocketThread.getTimeElapsed() + "[Thresold: " +
					 * caller.n.getRateThresold() + "]");
					 */
					/*
					 * if (caller.n.getUnderRatedNeighbours() >=
					 * Math.floor(Ant.maxNeighbours * Ant.underRateConnections) &&
					 * serverSocketThread.getTimeElapsed() >=
					 * caller.n.getRateThresold()) { throw new
					 * Exception(this.caller.serverSocket.getInetAddress().
					 * getHostAddress() + ": Rejected neighbour cause it doesn't
					 * satisfy bandwith request: [" +
					 * caller.n.getUnderRatedNeighbours() + "/" +
					 * Math.floor(Ant.maxNeighbours * Ant.underRateConnections) +
					 * "]"); }
					 */
					NeighbourAnt na = new NeighbourAnt(this.n, localSocket
							.getInetAddress()
							+ "", localSocket.getPort(), serverSocketThread
							.getRemoteServerPort(), serverSocketThread
							.getCipherEnc(), serverSocketThread.getCipherDec(),
							serverSocketThread.getSocket(), false,
							serverSocketThread.getTimeElapsed(),
							serverSocketThread.getURI());
					try {
						na.start();
						n.addNeighbour(na);
					} catch (Exception e) {
						na.terminate();
						na.setFailure();
						na.start();
						throw new Exception(e);
					}
					_logger
							.info(n.getShortId()
									+ " :Server added neighbour...");
				} else {
					_logger.info(n.getShortId() + " :Rejected connection...");
				}
			} else {
				localSocket.close();
				_logger.info(n.getShortId() + " :Rejected neighbour...");
			}
		} catch (Exception e) {
			_logger.error("New client handler: " /*
													 * +
													 * this.caller.serverSocket.getLocalPort()
													 */
					+ " ", e);
		}
	}
}

class AddressesRequestHandler extends Thread {
	Socket localSocket;
	Ant n;

	static Logger _logger = Logger.getLogger(NewClientHandler.class.getName());

	public AddressesRequestHandler(Ant caller, Socket localSocket) {
		this.localSocket = localSocket;
		this.n = caller;
		this.setPriority(5);
	}

	public void run() {
		try {
			String remoteAddress = localSocket.getInetAddress()
					.getHostAddress();
			if (!InetAddressWatchdog.getInstance().allowedAddress(
					InetAddress.getByName(remoteAddress).getHostAddress())) {
				_logger.info("Address is not allowed: " + remoteAddress);
				localSocket.close();
			} else {
				AddressServerThread serverThread = new AddressServerThread(
						localSocket);
				serverThread.start();
			}
		} catch (Exception e) {
			_logger.error("Addresses request handler: " /*
														 * +
														 * this.caller.serverSocket.getLocalPort()
														 */
					+ " ", e);
		}
	}
}

class DirectNeighbourRequestHandler extends Thread {
	Socket localSocket;
	Ant n;

	static Logger _logger = Logger.getLogger(NewClientHandler.class.getName());

	public DirectNeighbourRequestHandler(Ant caller, Socket localSocket) {
		this.localSocket = localSocket;
		this.n = caller;
	}

	public void run() {
		try {
			localSocket.setKeepAlive(true);
			localSocket.setSoTimeout(3 * 60000);
			TCPDirectServerThread tcpDirectServerThread = new TCPDirectServerThread(
					localSocket, n);
			tcpDirectServerThread.setPriority(10);
			tcpDirectServerThread.start();
		} catch (Exception e) {
			_logger.error("Direct neighbour request handler: " /*
																 * +
																 * this.caller.serverSocket.getLocalPort()
																 */
					+ " ", e);
		}
	}
}
